try:
    import zoneinfo
except ImportError:
    from backports import zoneinfo

from collections import OrderedDict
from datetime import date, datetime, timezone
from decimal import Decimal
from functools import partial

import pytest

from flask import Blueprint
from flask_restx import fields, Api


class FieldTestCase(object):
    field_class = None

    @pytest.fixture
    def api(self, app):
        blueprint = Blueprint("api", __name__)
        api = Api(blueprint)
        app.register_blueprint(blueprint)
        yield api

    def assert_field(self, field, value, expected):
        assert field.output("foo", {"foo": value}) == expected

    def assert_field_raises(self, field, value):
        with pytest.raises(fields.MarshallingError):
            field.output("foo", {"foo": value})


class BaseFieldTestMixin(object):
    def test_description(self):
        field = self.field_class(description="A description")
        assert "description" in field.__schema__
        assert field.__schema__["description"] == "A description"

    def test_title(self):
        field = self.field_class(title="A title")
        assert "title" in field.__schema__
        assert field.__schema__["title"] == "A title"

    def test_required(self):
        field = self.field_class(required=True)
        assert field.required

    def test_readonly(self):
        field = self.field_class(readonly=True)
        assert "readOnly" in field.__schema__
        assert field.__schema__["readOnly"]


class NumberTestMixin(object):
    def test_min(self):
        field = self.field_class(min=0)
        assert "minimum" in field.__schema__
        assert field.__schema__["minimum"] == 0
        assert "exclusiveMinimum" not in field.__schema__

    def test_min_as_callable(self):
        field = self.field_class(min=lambda: 0)
        assert "minimum" in field.__schema__
        assert field.__schema__["minimum"] == 0

    def test_min_exlusive(self):
        field = self.field_class(min=0, exclusiveMin=True)
        assert "minimum" in field.__schema__
        assert field.__schema__["minimum"] == 0
        assert "exclusiveMinimum" in field.__schema__
        assert field.__schema__["exclusiveMinimum"] is True

    def test_max(self):
        field = self.field_class(max=42)
        assert "maximum" in field.__schema__
        assert field.__schema__["maximum"] == 42
        assert "exclusiveMaximum" not in field.__schema__

    def test_max_as_callable(self):
        field = self.field_class(max=lambda: 42)
        assert "maximum" in field.__schema__
        assert field.__schema__["maximum"] == 42

    def test_max_exclusive(self):
        field = self.field_class(max=42, exclusiveMax=True)
        assert "maximum" in field.__schema__
        assert field.__schema__["maximum"] == 42
        assert "exclusiveMaximum" in field.__schema__
        assert field.__schema__["exclusiveMaximum"] is True

    def test_mulitple_of(self):
        field = self.field_class(multiple=5)
        assert "multipleOf" in field.__schema__
        assert field.__schema__["multipleOf"] == 5


class StringTestMixin(object):
    def test_min_length(self):
        field = self.field_class(min_length=1)
        assert "minLength" in field.__schema__
        assert field.__schema__["minLength"] == 1

    def test_min_length_as_callable(self):
        field = self.field_class(min_length=lambda: 1)
        assert "minLength" in field.__schema__
        assert field.__schema__["minLength"] == 1

    def test_max_length(self):
        field = self.field_class(max_length=42)
        assert "maxLength" in field.__schema__
        assert field.__schema__["maxLength"] == 42

    def test_max_length_as_callable(self):
        field = self.field_class(max_length=lambda: 42)
        assert "maxLength" in field.__schema__
        assert field.__schema__["maxLength"] == 42

    def test_pattern(self):
        field = self.field_class(pattern="[a-z]")
        assert "pattern" in field.__schema__
        assert field.__schema__["pattern"] == "[a-z]"


class RawFieldTest(BaseFieldTestMixin, FieldTestCase):
    """Test Raw field AND some common behaviors"""

    field_class = fields.Raw

    def test_type(self):
        field = fields.Raw()
        assert field.__schema__["type"] == "object"

    def test_default(self):
        field = fields.Raw(default="aaa")
        assert field.__schema__["default"] == "aaa"
        self.assert_field(field, None, "aaa")

    def test_default_as_callable(self):
        field = fields.Raw(default=lambda: "aaa")
        assert field.__schema__["default"] == "aaa"
        self.assert_field(field, None, "aaa")

    def test_with_attribute(self):
        field = fields.Raw(attribute="bar")
        assert field.output("foo", {"bar": 42}) == 42

    def test_with_lambda_attribute(self, mocker):
        obj = mocker.Mock()
        obj.value = 42
        field = fields.Raw(attribute=lambda x: x.value)
        assert field.output("foo", obj) == 42

    def test_with_partial_attribute(self, mocker):
        def f(x, suffix):
            return "{0}-{1}".format(x.value, suffix)

        obj = mocker.Mock()
        obj.value = 42

        p = partial(f, suffix="whatever")
        field = fields.Raw(attribute=p)
        assert field.output("foo", obj) == "42-whatever"

    def test_attribute_not_found(self):
        field = fields.Raw()
        assert field.output("foo", {"bar": 42}) is None

    def test_object(self, mocker):
        obj = mocker.Mock()
        obj.foo = 42
        field = fields.Raw()
        assert field.output("foo", obj) == 42

    def test_nested_object(self, mocker):
        foo = mocker.Mock()
        bar = mocker.Mock()
        bar.value = 42
        foo.bar = bar
        field = fields.Raw()
        assert field.output("bar.value", foo) == 42


class StringFieldTest(StringTestMixin, BaseFieldTestMixin, FieldTestCase):
    field_class = fields.String

    def test_defaults(self):
        field = fields.String()
        assert not field.required
        assert not field.discriminator
        assert field.__schema__ == {"type": "string"}

    def test_with_enum(self):
        enum = ["A", "B", "C"]
        field = fields.String(enum=enum)
        assert not field.required
        assert field.__schema__ == {"type": "string", "enum": enum, "example": enum[0]}

    def test_with_empty_enum(self):
        field = fields.String(enum=[])
        assert not field.required
        assert field.__schema__ == {"type": "string"}

    def test_with_callable_enum(self):
        enum = lambda: ["A", "B", "C"]  # noqa
        field = fields.String(enum=enum)
        assert not field.required
        assert field.__schema__ == {
            "type": "string",
            "enum": ["A", "B", "C"],
            "example": "A",
        }

    def test_with_empty_callable_enum(self):
        enum = lambda: []  # noqa
        field = fields.String(enum=enum)
        assert not field.required
        assert field.__schema__ == {"type": "string"}

    def test_with_default(self):
        field = fields.String(default="aaa")
        assert field.__schema__ == {"type": "string", "default": "aaa"}

    def test_string_field_with_discriminator(self):
        field = fields.String(discriminator=True)
        assert field.discriminator
        assert field.required
        assert field.__schema__ == {"type": "string"}

    def test_string_field_with_discriminator_override_require(self):
        field = fields.String(discriminator=True, required=False)
        assert field.discriminator
        assert field.required
        assert field.__schema__ == {"type": "string"}

    def test_discriminator_output(self, api):
        model = api.model(
            "Test",
            {
                "name": fields.String(discriminator=True),
            },
        )

        data = api.marshal({}, model)
        assert data == {"name": "Test"}

    def test_multiple_discriminator_field(self, api):
        model = api.model(
            "Test",
            {
                "name": fields.String(discriminator=True),
                "name2": fields.String(discriminator=True),
            },
        )

        with pytest.raises(ValueError):
            api.marshal(object(), model)

    @pytest.mark.parametrize(
        "value,expected",
        [
            ("string", "string"),
            (42, "42"),
        ],
    )
    def test_values(self, value, expected):
        self.assert_field(fields.String(), value, expected)


class IntegerFieldTest(BaseFieldTestMixin, NumberTestMixin, FieldTestCase):
    field_class = fields.Integer

    def test_defaults(self):
        field = fields.Integer()
        assert not field.required
        assert field.__schema__ == {"type": "integer"}

    def test_with_default(self):
        field = fields.Integer(default=42)
        assert not field.required
        assert field.__schema__ == {"type": "integer", "default": 42}
        self.assert_field(field, None, 42)

    @pytest.mark.parametrize(
        "value,expected",
        [
            (0, 0),
            (42, 42),
            ("42", 42),
            (None, None),
            (66.6, 66),
        ],
    )
    def test_value(self, value, expected):
        self.assert_field(fields.Integer(), value, expected)

    def test_decode_error_on_invalid_value(self):
        field = fields.Integer()
        self.assert_field_raises(field, "an int")

    def test_decode_error_on_invalid_type(self):
        field = fields.Integer()
        self.assert_field_raises(field, {"a": "dict"})


class BooleanFieldTest(BaseFieldTestMixin, FieldTestCase):
    field_class = fields.Boolean

    def test_defaults(self):
        field = fields.Boolean()
        assert not field.required
        assert field.__schema__ == {"type": "boolean"}

    def test_with_default(self):
        field = fields.Boolean(default=True)
        assert not field.required
        assert field.__schema__ == {"type": "boolean", "default": True}

    def test_with_example(self):
        field = fields.Boolean(default=True, example=False)
        assert field.__schema__ == {
            "type": "boolean",
            "default": True,
            "example": False,
        }

    @pytest.mark.parametrize(
        "value,expected",
        [
            (True, True),
            (False, False),
            ({}, False),
            ("false", False),  # These consistent with inputs.boolean
            ("0", False),
        ],
    )
    def test_value(self, value, expected):
        self.assert_field(fields.Boolean(), value, expected)


class FloatFieldTest(BaseFieldTestMixin, NumberTestMixin, FieldTestCase):
    field_class = fields.Float

    def test_defaults(self):
        field = fields.Float()
        assert not field.required
        assert field.__schema__ == {"type": "number"}

    def test_with_default(self):
        field = fields.Float(default=0.5)
        assert not field.required
        assert field.__schema__ == {"type": "number", "default": 0.5}

    def test_none_uses_default(self):
        field = fields.Float(default=0.5)
        assert not field.required
        assert field.__schema__ == {"type": "number", "default": 0.5}
        assert field.format(None) == 0.5

    @pytest.mark.parametrize(
        "value,expected",
        [
            ("-3.13", -3.13),
            (str(-3.13), -3.13),
            (3, 3.0),
        ],
    )
    def test_value(self, value, expected):
        self.assert_field(fields.Float(), value, expected)

    def test_raises(self):
        self.assert_field_raises(fields.Float(), "bar")

    def test_decode_error_on_invalid_value(self):
        field = fields.Float()
        self.assert_field_raises(field, "not a float")

    def test_decode_error_on_invalid_type(self):
        field = fields.Float()
        self.assert_field_raises(field, {"a": "dict"})


PI_STR = (
    "3.141592653589793238462643383279502884197169399375105820974944592307816406286208998628034825342117"
    "06798214808651328230664709384460955058223172535940812848111745028410270193852110555964462294895493"
    "038196442881097566593344612847564823378678316527120190914564856692346034861"
)

PI = Decimal(PI_STR)


class FixedFieldTest(BaseFieldTestMixin, NumberTestMixin, FieldTestCase):
    field_class = fields.Fixed

    def test_defaults(self):
        field = fields.Fixed()
        assert not field.required
        assert field.__schema__ == {"type": "number"}

    def test_with_default(self):
        field = fields.Fixed(default=0.5)
        assert not field.required
        assert field.__schema__ == {"type": "number", "default": 0.5}

    def test_fixed(self):
        field5 = fields.Fixed(5)
        field4 = fields.Fixed(4)

        self.assert_field(field5, PI, "3.14159")
        self.assert_field(field4, PI, "3.1416")
        self.assert_field(field4, 3, "3.0000")
        self.assert_field(field4, "03", "3.0000")
        self.assert_field(field4, "03.0", "3.0000")

    def test_zero(self):
        self.assert_field(fields.Fixed(), "0", "0.00000")

    def test_infinite(self):
        field = fields.Fixed()
        self.assert_field_raises(field, "+inf")
        self.assert_field_raises(field, "-inf")

    def test_nan(self):
        field = fields.Fixed()
        self.assert_field_raises(field, "NaN")


class ArbitraryFieldTest(BaseFieldTestMixin, NumberTestMixin, FieldTestCase):
    field_class = fields.Arbitrary

    def test_defaults(self):
        field = fields.Arbitrary()
        assert not field.required
        assert field.__schema__ == {"type": "number"}

    def test_with_default(self):
        field = fields.Arbitrary(default=0.5)
        assert field.__schema__ == {"type": "number", "default": 0.5}

    @pytest.mark.parametrize(
        "value,expected",
        [
            (PI_STR, PI_STR),
            (PI, PI_STR),
        ],
    )
    def test_value(self, value, expected):
        self.assert_field(fields.Arbitrary(), value, expected)


class DatetimeFieldTest(BaseFieldTestMixin, FieldTestCase):
    field_class = fields.DateTime

    def test_defaults(self):
        field = fields.DateTime()
        assert not field.required
        assert field.__schema__ == {"type": "string", "format": "date-time"}
        self.assert_field(field, None, None)

    def test_with_default(self):
        field = fields.DateTime(default="2014-08-25")
        assert field.__schema__ == {
            "type": "string",
            "format": "date-time",
            "default": "2014-08-25T00:00:00",
        }
        self.assert_field(field, None, "2014-08-25T00:00:00")

    def test_with_default_as_datetime(self):
        field = fields.DateTime(default=datetime(2014, 8, 25))
        assert field.__schema__ == {
            "type": "string",
            "format": "date-time",
            "default": "2014-08-25T00:00:00",
        }
        self.assert_field(field, None, "2014-08-25T00:00:00")

    def test_with_default_as_date(self):
        field = fields.DateTime(default=date(2014, 8, 25))
        assert field.__schema__ == {
            "type": "string",
            "format": "date-time",
            "default": "2014-08-25T00:00:00",
        }
        self.assert_field(field, None, "2014-08-25T00:00:00")

    def test_min(self):
        field = fields.DateTime(min="1984-06-07T00:00:00")
        assert "minimum" in field.__schema__
        assert field.__schema__["minimum"] == "1984-06-07T00:00:00"
        assert "exclusiveMinimum" not in field.__schema__

    def test_min_as_date(self):
        field = fields.DateTime(min=date(1984, 6, 7))
        assert "minimum" in field.__schema__
        assert field.__schema__["minimum"] == "1984-06-07T00:00:00"
        assert "exclusiveMinimum" not in field.__schema__

    def test_min_as_datetime(self):
        field = fields.DateTime(min=datetime(1984, 6, 7, 1, 2, 0))
        assert "minimum" in field.__schema__
        assert field.__schema__["minimum"] == "1984-06-07T01:02:00"
        assert "exclusiveMinimum" not in field.__schema__

    def test_min_exlusive(self):
        field = fields.DateTime(min="1984-06-07T00:00:00", exclusiveMin=True)
        assert "minimum" in field.__schema__
        assert field.__schema__["minimum"] == "1984-06-07T00:00:00"
        assert "exclusiveMinimum" in field.__schema__
        assert field.__schema__["exclusiveMinimum"] is True

    def test_max(self):
        field = fields.DateTime(max="1984-06-07T00:00:00")
        assert "maximum" in field.__schema__
        assert field.__schema__["maximum"] == "1984-06-07T00:00:00"
        assert "exclusiveMaximum" not in field.__schema__

    def test_max_as_date(self):
        field = fields.DateTime(max=date(1984, 6, 7))
        assert "maximum" in field.__schema__
        assert field.__schema__["maximum"] == "1984-06-07T00:00:00"
        assert "exclusiveMaximum" not in field.__schema__

    def test_max_as_datetime(self):
        field = fields.DateTime(max=datetime(1984, 6, 7, 1, 2, 0))
        assert "maximum" in field.__schema__
        assert field.__schema__["maximum"] == "1984-06-07T01:02:00"
        assert "exclusiveMaximum" not in field.__schema__

    def test_max_exclusive(self):
        field = fields.DateTime(max="1984-06-07T00:00:00", exclusiveMax=True)
        assert "maximum" in field.__schema__
        assert field.__schema__["maximum"] == "1984-06-07T00:00:00"
        assert "exclusiveMaximum" in field.__schema__
        assert field.__schema__["exclusiveMaximum"] is True

    @pytest.mark.parametrize(
        "value,expected",
        [
            (date(2011, 1, 1), "Sat, 01 Jan 2011 00:00:00 -0000"),
            (datetime(2011, 1, 1), "Sat, 01 Jan 2011 00:00:00 -0000"),
            (datetime(2011, 1, 1, 23, 59, 59), "Sat, 01 Jan 2011 23:59:59 -0000"),
            (
                datetime(2011, 1, 1, 23, 59, 59, tzinfo=timezone.utc),
                "Sat, 01 Jan 2011 23:59:59 -0000",
            ),
            (
                datetime(2011, 1, 1, 23, 59, 59, tzinfo=zoneinfo.ZoneInfo("CET")),
                "Sat, 01 Jan 2011 22:59:59 -0000",
            ),
        ],
    )
    def test_rfc822_value(self, value, expected):
        self.assert_field(fields.DateTime(dt_format="rfc822"), value, expected)

    @pytest.mark.parametrize(
        "value,expected",
        [
            (date(2011, 1, 1), "2011-01-01T00:00:00"),
            (datetime(2011, 1, 1), "2011-01-01T00:00:00"),
            (datetime(2011, 1, 1, 23, 59, 59), "2011-01-01T23:59:59"),
            (datetime(2011, 1, 1, 23, 59, 59, 1000), "2011-01-01T23:59:59.001000"),
            (
                datetime(2011, 1, 1, 23, 59, 59, tzinfo=timezone.utc),
                "2011-01-01T23:59:59+00:00",
            ),
            (
                datetime(2011, 1, 1, 23, 59, 59, 1000, tzinfo=timezone.utc),
                "2011-01-01T23:59:59.001000+00:00",
            ),
            (
                datetime(2011, 1, 1, 23, 59, 59, tzinfo=zoneinfo.ZoneInfo("CET")),
                "2011-01-01T23:59:59+01:00",
            ),
        ],
    )
    def test_iso8601_value(self, value, expected):
        self.assert_field(fields.DateTime(dt_format="iso8601"), value, expected)

    def test_unsupported_format(self):
        field = fields.DateTime(dt_format="raw")
        self.assert_field_raises(field, datetime.now())

    def test_unsupported_value_format(self):
        field = fields.DateTime(dt_format="raw")
        self.assert_field_raises(field, "xxx")


class DateFieldTest(BaseFieldTestMixin, FieldTestCase):
    field_class = fields.Date

    def test_defaults(self):
        field = fields.Date()
        assert not field.required
        assert field.__schema__ == {"type": "string", "format": "date"}

    def test_with_default(self):
        field = fields.Date(default="2014-08-25")
        assert field.__schema__ == {
            "type": "string",
            "format": "date",
            "default": "2014-08-25",
        }
        self.assert_field(field, None, "2014-08-25")

    def test_with_default_as_date(self):
        field = fields.Date(default=date(2014, 8, 25))
        assert field.__schema__ == {
            "type": "string",
            "format": "date",
            "default": "2014-08-25",
        }

    def test_with_default_as_datetime(self):
        field = fields.Date(default=datetime(2014, 8, 25))
        assert field.__schema__ == {
            "type": "string",
            "format": "date",
            "default": "2014-08-25",
        }

    def test_min(self):
        field = fields.Date(min="1984-06-07")
        assert "minimum" in field.__schema__
        assert field.__schema__["minimum"] == "1984-06-07"
        assert "exclusiveMinimum" not in field.__schema__

    def test_min_as_date(self):
        field = fields.Date(min=date(1984, 6, 7))
        assert "minimum" in field.__schema__
        assert field.__schema__["minimum"] == "1984-06-07"
        assert "exclusiveMinimum" not in field.__schema__

    def test_min_as_datetime(self):
        field = fields.Date(min=datetime(1984, 6, 7, 1, 2, 0))
        assert "minimum" in field.__schema__
        assert field.__schema__["minimum"] == "1984-06-07"
        assert "exclusiveMinimum" not in field.__schema__

    def test_min_exlusive(self):
        field = fields.Date(min="1984-06-07", exclusiveMin=True)
        assert "minimum" in field.__schema__
        assert field.__schema__["minimum"] == "1984-06-07"
        assert "exclusiveMinimum" in field.__schema__
        assert field.__schema__["exclusiveMinimum"] is True

    def test_max(self):
        field = fields.Date(max="1984-06-07")
        assert "maximum" in field.__schema__
        assert field.__schema__["maximum"] == "1984-06-07"
        assert "exclusiveMaximum" not in field.__schema__

    def test_max_as_date(self):
        field = fields.Date(max=date(1984, 6, 7))
        assert "maximum" in field.__schema__
        assert field.__schema__["maximum"] == "1984-06-07"
        assert "exclusiveMaximum" not in field.__schema__

    def test_max_as_datetime(self):
        field = fields.Date(max=datetime(1984, 6, 7, 1, 2, 0))
        assert "maximum" in field.__schema__
        assert field.__schema__["maximum"] == "1984-06-07"
        assert "exclusiveMaximum" not in field.__schema__

    def test_max_exclusive(self):
        field = fields.Date(max="1984-06-07", exclusiveMax=True)
        assert "maximum" in field.__schema__
        assert field.__schema__["maximum"] == "1984-06-07"
        assert "exclusiveMaximum" in field.__schema__
        assert field.__schema__["exclusiveMaximum"] is True

    @pytest.mark.parametrize(
        "value,expected",
        [
            (date(2011, 1, 1), "2011-01-01"),
            (datetime(2011, 1, 1), "2011-01-01"),
            (datetime(2011, 1, 1, 23, 59, 59), "2011-01-01"),
            (datetime(2011, 1, 1, 23, 59, 59, 1000), "2011-01-01"),
            (datetime(2011, 1, 1, 23, 59, 59, tzinfo=timezone.utc), "2011-01-01"),
            (datetime(2011, 1, 1, 23, 59, 59, 1000, tzinfo=timezone.utc), "2011-01-01"),
            (
                datetime(2011, 1, 1, 23, 59, 59, tzinfo=zoneinfo.ZoneInfo("CET")),
                "2011-01-01",
            ),
        ],
    )
    def test_value(self, value, expected):
        self.assert_field(fields.Date(), value, expected)

    def test_unsupported_value_format(self):
        self.assert_field_raises(fields.Date(), "xxx")


class FormatedStringFieldTest(StringTestMixin, BaseFieldTestMixin, FieldTestCase):
    field_class = partial(fields.FormattedString, "Hello {name}")

    def test_defaults(self):
        field = fields.FormattedString("Hello {name}")
        assert not field.required
        assert field.__schema__ == {"type": "string"}

    def test_dict(self):
        data = {
            "sid": 3,
            "account_sid": 4,
        }
        field = fields.FormattedString("/foo/{account_sid}/{sid}/")
        assert field.output("foo", data) == "/foo/4/3/"

    def test_object(self, mocker):
        obj = mocker.Mock()
        obj.sid = 3
        obj.account_sid = 4
        field = fields.FormattedString("/foo/{account_sid}/{sid}/")
        assert field.output("foo", obj) == "/foo/4/3/"

    def test_none(self):
        field = fields.FormattedString("{foo}")
        # self.assert_field_raises(field, None)
        with pytest.raises(fields.MarshallingError):
            field.output("foo", None)

    def test_invalid_object(self):
        field = fields.FormattedString("/foo/{0[account_sid]}/{0[sid]}/")
        self.assert_field_raises(field, {})

    def test_tuple(self):
        field = fields.FormattedString("/foo/{0[account_sid]}/{0[sid]}/")
        self.assert_field_raises(field, (3, 4))


class UrlFieldTest(StringTestMixin, BaseFieldTestMixin, FieldTestCase):
    field_class = partial(fields.Url, "endpoint")

    def test_defaults(self):
        field = fields.Url("endpoint")
        assert not field.required
        assert field.__schema__ == {"type": "string"}

    def test_invalid_object(self, app):
        app.add_url_rule("/<foo>", "foobar", view_func=lambda x: x)
        field = fields.Url("foobar")

        with app.test_request_context("/"):
            with pytest.raises(fields.MarshallingError):
                field.output("foo", None)

    def test_simple(self, app, mocker):
        app.add_url_rule("/<foo>", "foobar", view_func=lambda x: x)
        field = fields.Url("foobar")
        obj = mocker.Mock(foo=42)

        with app.test_request_context("/"):
            assert "/42" == field.output("foo", obj)

    def test_absolute(self, app, mocker):
        app.add_url_rule("/<foo>", "foobar", view_func=lambda x: x)
        field = fields.Url("foobar", absolute=True)
        obj = mocker.Mock(foo=42)

        with app.test_request_context("/"):
            assert "http://localhost/42" == field.output("foo", obj)

    def test_absolute_scheme(self, app, mocker):
        """Url.scheme should override current_request.scheme"""
        app.add_url_rule("/<foo>", "foobar", view_func=lambda x: x)
        field = fields.Url("foobar", absolute=True, scheme="https")
        obj = mocker.Mock(foo=42)

        with app.test_request_context("/", base_url="http://localhost"):
            assert "https://localhost/42" == field.output("foo", obj)

    def test_without_endpoint_invalid_object(self, app):
        app.add_url_rule("/<foo>", "foobar", view_func=lambda x: x)
        field = fields.Url()

        with app.test_request_context("/foo"):
            with pytest.raises(fields.MarshallingError):
                field.output("foo", None)

    def test_without_endpoint(self, app, mocker):
        app.add_url_rule("/<foo>", "foobar", view_func=lambda x: x)
        field = fields.Url()
        obj = mocker.Mock(foo=42)

        with app.test_request_context("/foo"):
            assert "/42" == field.output("foo", obj)

    def test_without_endpoint_absolute(self, app, mocker):
        app.add_url_rule("/<foo>", "foobar", view_func=lambda x: x)
        field = fields.Url(absolute=True)
        obj = mocker.Mock(foo=42)

        with app.test_request_context("/foo"):
            assert "http://localhost/42" == field.output("foo", obj)

    def test_without_endpoint_absolute_scheme(self, app, mocker):
        app.add_url_rule("/<foo>", "foobar", view_func=lambda x: x)
        field = fields.Url(absolute=True, scheme="https")
        obj = mocker.Mock(foo=42)

        with app.test_request_context("/foo", base_url="http://localhost"):
            assert "https://localhost/42" == field.output("foo", obj)

    def test_with_blueprint_invalid_object(self, app):
        bp = Blueprint("foo", __name__, url_prefix="/foo")
        bp.add_url_rule("/<foo>", "foobar", view_func=lambda x: x)
        app.register_blueprint(bp)
        field = fields.Url()

        with app.test_request_context("/foo/foo"):
            with pytest.raises(fields.MarshallingError):
                field.output("foo", None)

    def test_with_blueprint(self, app, mocker):
        bp = Blueprint("foo", __name__, url_prefix="/foo")
        bp.add_url_rule("/<foo>", "foobar", view_func=lambda x: x)
        app.register_blueprint(bp)
        field = fields.Url()
        obj = mocker.Mock(foo=42)

        with app.test_request_context("/foo/foo"):
            assert "/foo/42" == field.output("foo", obj)

    def test_with_blueprint_absolute(self, app, mocker):
        bp = Blueprint("foo", __name__, url_prefix="/foo")
        bp.add_url_rule("/<foo>", "foobar", view_func=lambda x: x)
        app.register_blueprint(bp)
        field = fields.Url(absolute=True)
        obj = mocker.Mock(foo=42)

        with app.test_request_context("/foo/foo"):
            assert "http://localhost/foo/42" == field.output("foo", obj)

    def test_with_blueprint_absolute_scheme(self, app, mocker):
        bp = Blueprint("foo", __name__, url_prefix="/foo")
        bp.add_url_rule("/<foo>", "foobar", view_func=lambda x: x)
        app.register_blueprint(bp)
        field = fields.Url(absolute=True, scheme="https")
        obj = mocker.Mock(foo=42)

        with app.test_request_context("/foo/foo", base_url="http://localhost"):
            assert "https://localhost/foo/42" == field.output("foo", obj)


class NestedFieldTest(FieldTestCase):
    def test_defaults(self, api):
        nested_fields = api.model("NestedModel", {"name": fields.String})
        field = fields.Nested(nested_fields)
        assert not field.required
        assert field.__schema__ == {"$ref": "#/definitions/NestedModel"}

    def test_with_required(self, api):
        nested_fields = api.model("NestedModel", {"name": fields.String})
        field = fields.Nested(nested_fields, required=True)
        assert field.required
        assert not field.allow_null
        assert field.__schema__ == {"$ref": "#/definitions/NestedModel"}

    def test_with_description(self, api):
        nested_fields = api.model("NestedModel", {"name": fields.String})
        field = fields.Nested(nested_fields, description="A description")
        assert field.__schema__ == {
            "description": "A description",
            "allOf": [{"$ref": "#/definitions/NestedModel"}],
        }

    def test_with_title(self, api):
        nested_fields = api.model("NestedModel", {"name": fields.String})
        field = fields.Nested(nested_fields, title="A title")
        assert field.__schema__ == {
            "title": "A title",
            "allOf": [{"$ref": "#/definitions/NestedModel"}],
        }

    def test_with_allow_null(self, api):
        nested_fields = api.model("NestedModel", {"name": fields.String})
        field = fields.Nested(nested_fields, allow_null=True)
        assert not field.required
        assert field.allow_null
        assert field.__schema__ == {"$ref": "#/definitions/NestedModel"}

    def test_with_nullable_schema(self, api):
        nested_fields = api.model("NestedModel", {"name": fields.String})
        field = fields.Nested(nested_fields, nullable=True)
        # Should allow null in schema via anyOf
        assert field.__schema__ == {
            "anyOf": [
                {"$ref": "#/definitions/NestedModel"},
                {"type": "null"},
            ]
        }

    def test_with_skip_none(self, api):
        nested_fields = api.model("NestedModel", {"name": fields.String})
        field = fields.Nested(nested_fields, skip_none=True)
        assert not field.required
        assert field.skip_none
        assert field.__schema__ == {"$ref": "#/definitions/NestedModel"}

    def test_with_readonly(self, app):
        api = Api(app)
        nested_fields = api.model("NestedModel", {"name": fields.String})
        field = fields.Nested(nested_fields, readonly=True)
        assert field.__schema__ == {
            "readOnly": True,
            "allOf": [{"$ref": "#/definitions/NestedModel"}],
        }

    def test_as_list(self, api):
        nested_fields = api.model("NestedModel", {"name": fields.String})
        field = fields.Nested(nested_fields, as_list=True)
        assert field.as_list
        assert field.__schema__ == {
            "type": "array",
            "items": {"$ref": "#/definitions/NestedModel"},
        }

    def test_as_list_is_reusable(self, api):
        nested_fields = api.model("NestedModel", {"name": fields.String})

        field = fields.Nested(nested_fields, as_list=True)
        assert field.__schema__ == {
            "type": "array",
            "items": {"$ref": "#/definitions/NestedModel"},
        }

        field = fields.Nested(nested_fields)
        assert field.__schema__ == {"$ref": "#/definitions/NestedModel"}


class ListFieldTest(BaseFieldTestMixin, FieldTestCase):
    field_class = partial(fields.List, fields.String)

    def test_defaults(self):
        field = fields.List(fields.String)
        assert not field.required
        assert field.__schema__ == {"type": "array", "items": {"type": "string"}}

    def test_with_nested_field(self, api):
        nested_fields = api.model("NestedModel", {"name": fields.String})
        field = fields.List(fields.Nested(nested_fields))
        assert field.__schema__ == {
            "type": "array",
            "items": {"$ref": "#/definitions/NestedModel"},
        }

        data = [{"name": "John Doe", "age": 42}, {"name": "Jane Doe", "age": 66}]
        expected = [
            OrderedDict([("name", "John Doe")]),
            OrderedDict([("name", "Jane Doe")]),
        ]
        self.assert_field(field, data, expected)

    def test_min_items(self):
        field = fields.List(fields.String, min_items=5)
        assert "minItems" in field.__schema__
        assert field.__schema__["minItems"] == 5

    def test_max_items(self):
        field = fields.List(fields.String, max_items=42)
        assert "maxItems" in field.__schema__
        assert field.__schema__["maxItems"] == 42

    def test_unique(self):
        field = fields.List(fields.String, unique=True)
        assert "uniqueItems" in field.__schema__
        assert field.__schema__["uniqueItems"] is True

    @pytest.mark.parametrize(
        "value,expected",
        [
            (["a", "b", "c"], ["a", "b", "c"]),
            (["c", "b", "a"], ["c", "b", "a"]),
            (("a", "b", "c"), ["a", "b", "c"]),
            (["a"], ["a"]),
            (None, None),
        ],
    )
    def test_value(self, value, expected):
        self.assert_field(fields.List(fields.String()), value, expected)

    def test_with_set(self):
        field = fields.List(fields.String)
        value = set(["a", "b", "c"])
        output = field.output("foo", {"foo": value})
        assert set(output) == value

    def test_with_scoped_attribute_on_dict_or_obj(self):
        class Test(object):
            def __init__(self, data):
                self.data = data

        class Nested(object):
            def __init__(self, value):
                self.value = value

        nesteds = [Nested(i) for i in ["a", "b", "c"]]
        test_obj = Test(nesteds)
        test_dict = {"data": [{"value": "a"}, {"value": "b"}, {"value": "c"}]}

        field = fields.List(fields.String(attribute="value"), attribute="data")
        assert ["a" == "b", "c"], field.output("whatever", test_obj)
        assert ["a" == "b", "c"], field.output("whatever", test_dict)

    def test_with_attribute(self):
        data = [{"a": 1, "b": 1}, {"a": 2, "b": 1}, {"a": 3, "b": 1}]
        field = fields.List(fields.Integer(attribute="a"))
        self.assert_field(field, data, [1, 2, 3])

    def test_list_of_raw(self):
        field = fields.List(fields.Raw)

        data = [{"a": 1, "b": 1}, {"a": 2, "b": 1}, {"a": 3, "b": 1}]
        expected = [
            OrderedDict([("a", 1), ("b", 1)]),
            OrderedDict([("a", 2), ("b", 1)]),
            OrderedDict([("a", 3), ("b", 1)]),
        ]
        self.assert_field(field, data, expected)

        data = [1, 2, "a"]
        self.assert_field(field, data, data)


class WildcardFieldTest(BaseFieldTestMixin, FieldTestCase):
    field_class = partial(fields.Wildcard, fields.String)

    def test_types(self):
        with pytest.raises(fields.MarshallingError):

            class WrongType:
                pass

            x = WrongType()
            field1 = fields.Wildcard(WrongType)  # noqa
            field2 = fields.Wildcard(x)  # noqa

    def test_defaults(self):
        field = fields.Wildcard(fields.String)
        assert not field.required
        assert field.__schema__ == {
            "type": "object",
            "additionalProperties": {"type": "string"},
        }

    def test_with_scoped_attribute_on_dict_or_obj(self):
        class Test(object):
            def __init__(self, data):
                self.data = data

        class Nested(object):
            def __init__(self, value):
                self.value = value

        nesteds = [Nested(i) for i in ["a", "b", "c"]]
        test_obj = Test(nesteds)
        test_dict = {"data": [{"value": "a"}, {"value": "b"}, {"value": "c"}]}

        field = fields.Wildcard(fields.String(attribute="value"), attribute="data")
        assert ["a" == "b", "c"], field.output("whatever", test_obj)
        assert ["a" == "b", "c"], field.output("whatever", test_dict)

    def test_list_of_raw(self):
        field = fields.Wildcard(fields.Raw)

        data = [{"a": 1, "b": 1}, {"a": 2, "b": 1}, {"a": 3, "b": 1}]
        expected = [
            OrderedDict([("a", 1), ("b", 1)]),
            OrderedDict([("a", 2), ("b", 1)]),
            OrderedDict([("a", 3), ("b", 1)]),
        ]
        self.assert_field(field, data, expected)

        data = [1, 2, "a"]
        self.assert_field(field, data, data)

    def test_wildcard(self, api):
        wild1 = fields.Wildcard(fields.String)
        wild2 = fields.Wildcard(fields.Integer)
        wild3 = fields.Wildcard(fields.String)
        wild4 = fields.Wildcard(fields.String, default="x")
        wild5 = fields.Wildcard(fields.String)
        wild6 = fields.Wildcard(fields.Integer)
        wild7 = fields.Wildcard(fields.String)
        wild8 = fields.Wildcard(fields.String)

        mod5 = OrderedDict()
        mod5["toto"] = fields.Integer
        mod5["bob"] = fields.Integer
        mod5["*"] = wild5

        wild_fields1 = api.model("WildcardModel1", {"*": wild1})
        wild_fields2 = api.model("WildcardModel2", {"j*": wild2})
        wild_fields3 = api.model("WildcardModel3", {"*": wild3})
        wild_fields4 = api.model("WildcardModel4", {"*": wild4})
        wild_fields5 = api.model("WildcardModel5", mod5)
        wild_fields6 = api.model(
            "WildcardModel6",
            {
                "nested": {
                    "f1": fields.String(default="12"),
                    "f2": fields.Integer(default=13),
                },
                "a*": wild6,
            },
        )
        wild_fields7 = api.model("WildcardModel7", {"*": wild7})
        wild_fields8 = api.model("WildcardModel8", {"*": wild8})

        class Dummy(object):
            john = 12
            bob = "42"
            alice = None

        class Dummy2(object):
            pass

        class Dummy3(object):
            a = None
            b = None

        data = {"John": 12, "bob": 42, "Jane": "68"}
        data3 = Dummy()
        data4 = Dummy2()
        data5 = {"John": 12, "bob": 42, "Jane": "68", "toto": "72"}
        data6 = {"nested": {"f1": 12, "f2": 13}, "alice": "14"}
        data7 = Dummy3()
        data8 = None
        expected1 = {"John": "12", "bob": "42", "Jane": "68"}
        expected2 = {"John": 12, "Jane": 68}
        expected3 = {"john": "12", "bob": "42"}
        expected4 = {"*": "x"}
        expected5 = {"John": "12", "bob": 42, "Jane": "68", "toto": 72}
        expected6 = {"nested": {"f1": "12", "f2": 13}, "alice": 14}
        expected7 = {}
        expected8 = {}

        result1 = api.marshal(data, wild_fields1)
        result2 = api.marshal(data, wild_fields2)
        result3 = api.marshal(data3, wild_fields3, skip_none=True)
        result4 = api.marshal(data4, wild_fields4)
        result5 = api.marshal(data5, wild_fields5)
        result6 = api.marshal(data6, wild_fields6)
        result7 = api.marshal(data7, wild_fields7, skip_none=True)
        result8 = api.marshal(data8, wild_fields8, skip_none=True)

        assert expected1 == result1
        assert expected2 == result2
        assert expected3 == result3
        assert expected4 == result4
        assert expected5 == result5
        assert expected6 == result6
        assert expected7 == result7
        assert expected8 == result8

    def test_clone(self, api):
        wild1 = fields.Wildcard(fields.String)
        wild2 = wild1.clone()

        wild_fields1 = api.model("cloneWildcard1", {"*": wild1})
        wild_fields2 = api.model("cloneWildcard2", {"*": wild2})

        data = {"John": 12, "bob": 42, "Jane": "68"}
        expected1 = {"John": "12", "bob": "42", "Jane": "68"}

        result1 = api.marshal(data, wild_fields1)
        result2 = api.marshal(data, wild_fields2)

        assert expected1 == result1
        assert result2 == result1


class ClassNameFieldTest(StringTestMixin, BaseFieldTestMixin, FieldTestCase):
    field_class = fields.ClassName

    def test_simple_string_field(self):
        field = fields.ClassName()
        assert not field.required
        assert not field.discriminator
        assert field.__schema__ == {"type": "string"}

    def test_default_output_classname(self, api):
        model = api.model(
            "Test",
            {
                "name": fields.ClassName(),
            },
        )

        class FakeClass(object):
            pass

        data = api.marshal(FakeClass(), model)
        assert data == {"name": "FakeClass"}

    def test_output_dash(self, api):
        model = api.model(
            "Test",
            {
                "name": fields.ClassName(dash=True),
            },
        )

        class FakeClass(object):
            pass

        data = api.marshal(FakeClass(), model)
        assert data == {"name": "fake_class"}

    def test_with_dict(self, api):
        model = api.model(
            "Test",
            {
                "name": fields.ClassName(),
            },
        )

        data = api.marshal({}, model)
        assert data == {"name": "object"}


class PolymorphTest(FieldTestCase):
    def test_polymorph_field(self, api):
        parent = api.model(
            "Person",
            {
                "name": fields.String,
            },
        )

        child1 = api.inherit(
            "Child1",
            parent,
            {
                "extra1": fields.String,
            },
        )

        child2 = api.inherit(
            "Child2",
            parent,
            {
                "extra2": fields.String,
            },
        )

        class Child1(object):
            name = "child1"
            extra1 = "extra1"

        class Child2(object):
            name = "child2"
            extra2 = "extra2"

        mapping = {Child1: child1, Child2: child2}

        thing = api.model(
            "Thing",
            {
                "owner": fields.Polymorph(mapping),
            },
        )

        def data(cls):
            return api.marshal({"owner": cls()}, thing)

        assert data(Child1) == {"owner": {"name": "child1", "extra1": "extra1"}}

        assert data(Child2) == {"owner": {"name": "child2", "extra2": "extra2"}}

    def test_polymorph_field_no_common_ancestor(self, api):
        child1 = api.model(
            "Child1",
            {
                "extra1": fields.String,
            },
        )

        child2 = api.model(
            "Child2",
            {
                "extra2": fields.String,
            },
        )

        class Child1(object):
            pass

        class Child2(object):
            pass

        mapping = {Child1: child1, Child2: child2}

        with pytest.raises(ValueError):
            fields.Polymorph(mapping)

    def test_polymorph_field_unknown_class(self, api):
        parent = api.model(
            "Person",
            {
                "name": fields.String,
            },
        )

        child1 = api.inherit(
            "Child1",
            parent,
            {
                "extra1": fields.String,
            },
        )

        child2 = api.inherit(
            "Child2",
            parent,
            {
                "extra2": fields.String,
            },
        )

        class Child1(object):
            name = "child1"
            extra1 = "extra1"

        class Child2(object):
            name = "child2"
            extra2 = "extra2"

        mapping = {Child1: child1, Child2: child2}

        thing = api.model(
            "Thing",
            {
                "owner": fields.Polymorph(mapping),
            },
        )

        with pytest.raises(ValueError):
            api.marshal({"owner": object()}, thing)

    def test_polymorph_field_does_not_have_ambiguous_mappings(self, api):
        """
        Regression test for https://github.com/noirbizarre/flask-restx/pull/691
        """
        parent = api.model(
            "Parent",
            {
                "name": fields.String,
            },
        )

        child = api.inherit(
            "Child",
            parent,
            {
                "extra": fields.String,
            },
        )

        class Parent(object):
            name = "parent"

        class Child(Parent):
            extra = "extra"

        mapping = {Parent: parent, Child: child}

        thing = api.model(
            "Thing",
            {
                "owner": fields.Polymorph(mapping),
            },
        )

        api.marshal({"owner": Child()}, thing)

    def test_polymorph_field_required_default(self, api):
        parent = api.model(
            "Person",
            {
                "name": fields.String,
            },
        )

        child1 = api.inherit(
            "Child1",
            parent,
            {
                "extra1": fields.String,
            },
        )

        child2 = api.inherit(
            "Child2",
            parent,
            {
                "extra2": fields.String,
            },
        )

        class Child1(object):
            name = "child1"
            extra1 = "extra1"

        class Child2(object):
            name = "child2"
            extra2 = "extra2"

        mapping = {Child1: child1, Child2: child2}

        thing = api.model(
            "Thing",
            {
                "owner": fields.Polymorph(
                    mapping, required=True, default={"name": "default"}
                ),
            },
        )

        data = api.marshal({}, thing)

        assert data == {"owner": {"name": "default"}}

    def test_polymorph_field_not_required(self, api):
        parent = api.model(
            "Person",
            {
                "name": fields.String,
            },
        )

        child1 = api.inherit(
            "Child1",
            parent,
            {
                "extra1": fields.String,
            },
        )

        child2 = api.inherit(
            "Child2",
            parent,
            {
                "extra2": fields.String,
            },
        )

        class Child1(object):
            name = "child1"
            extra1 = "extra1"

        class Child2(object):
            name = "child2"
            extra2 = "extra2"

        mapping = {Child1: child1, Child2: child2}

        thing = api.model(
            "Thing",
            {
                "owner": fields.Polymorph(mapping),
            },
        )

        data = api.marshal({}, thing)

        assert data == {"owner": None}

    def test_polymorph_with_discriminator(self, api):
        parent = api.model(
            "Person",
            {
                "name": fields.String,
                "model": fields.String(discriminator=True),
            },
        )

        child1 = api.inherit(
            "Child1",
            parent,
            {
                "extra1": fields.String,
            },
        )

        child2 = api.inherit(
            "Child2",
            parent,
            {
                "extra2": fields.String,
            },
        )

        class Child1(object):
            name = "child1"
            extra1 = "extra1"

        class Child2(object):
            name = "child2"
            extra2 = "extra2"

        mapping = {Child1: child1, Child2: child2}

        thing = api.model(
            "Thing",
            {
                "owner": fields.Polymorph(mapping),
            },
        )

        def data(cls):
            return api.marshal({"owner": cls()}, thing)

        assert data(Child1) == {
            "owner": {"name": "child1", "model": "Child1", "extra1": "extra1"}
        }

        assert data(Child2) == {
            "owner": {"name": "child2", "model": "Child2", "extra2": "extra2"}
        }


class CustomFieldTest(FieldTestCase):
    def test_custom_field(self):
        class CustomField(fields.Integer):
            __schema_format__ = "int64"

        field = CustomField()

        assert field.__schema__ == {"type": "integer", "format": "int64"}


class FieldsHelpersTest(object):
    def test_to_dict(self):
        expected = data = {"foo": 42}
        assert fields.to_marshallable_type(data) == expected

    def test_to_dict_obj(self):
        class Foo(object):
            def __init__(self):
                self.foo = 42

        expected = {"foo": 42}
        assert fields.to_marshallable_type(Foo()) == expected

    def test_to_dict_custom_marshal(self):
        class Foo(object):
            def __marshallable__(self):
                return {"foo": 42}

        expected = {"foo": 42}
        assert fields.to_marshallable_type(Foo()) == expected

    def test_get_value(self):
        assert fields.get_value("foo", {"foo": 42}) == 42

    def test_get_value_no_value(self):
        assert fields.get_value("foo", {"foo": 42}) == 42

    def test_get_value_obj(self, mocker):
        assert fields.get_value("foo", mocker.Mock(foo=42)) == 42

    def test_get_value_indexable_object(self):
        class Test(object):
            def __init__(self, value):
                self.value = value

            def __getitem__(self, n):
                if type(n) is int:
                    if n < 3:
                        return n
                    raise IndexError
                raise TypeError

        obj = Test("hi")
        assert fields.get_value("value", obj) == "hi"

    def test_get_value_int_indexable_list(self):
        assert fields.get_value("bar.0", {"bar": [42]}) == 42

    def test_get_value_int_indexable_list_with_str(self):
        assert fields.get_value("bar.abc", {"bar": [42]}) is None

    def test_get_value_int_indexable_nested_list(self):
        assert fields.get_value("bar.0.val", {"bar": [{"val": 42}]}) == 42

    def test_get_value_int_indexable_tuple_with_str(self):
        assert fields.get_value("bar.abc", {"bar": (42, 43)}) is None

    def test_get_value_int_indexable_tuple(self):
        assert fields.get_value("bar.0", {"bar": (42, 43)}) == 42

    def test_get_value_int_indexable_nested_tuple(self):
        assert fields.get_value("bar.0.val", {"bar": [{"val": 42}]}) == 42
